<!doctype html>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1183461
-->
<!--
  This test is similar to those in test_animations.html with the exception
  that those tests interact with a single element at a time. The tests in this
  file are specifically concerned with testing the ordering of events between
  elements for which most of the utilities in animation_utils.js are not
  suited.
-->
<head>
  <meta charset=utf-8>
  <title>Test for CSS Animation and Transition event ordering
         (Bug 1183461)</title>
  <script type="application/javascript"
    src="/tests/SimpleTest/SimpleTest.js"></script>
  <!-- We still need animation_utils.js for advance_clock -->
  <script type="application/javascript" src="animation_utils.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
  <style>
  @keyframes anim  { to { margin-left: 100px } }
  @keyframes animA { to { margin-left: 100px } }
  @keyframes animB { to { margin-left: 100px } }
  @keyframes animC { to { margin-left: 100px } }
  </style>
</head>
<body>
<a target="_blank"
  href="https://bugzilla.mozilla.org/show_bug.cgi?id=1183461">Mozilla Bug
  1183461</a>
<div id="display"></div>
<pre id="test">
<script type="application/javascript">
'use strict';

// Take over the refresh driver right from the start.
advance_clock(0);

// Common test scaffolding

var gEventsReceived = [];
var gDisplay = document.getElementById('display');

[ 'animationstart',
  'animationiteration',
  'animationend',
  'transitionrun',
  'transitionstart',
  'transitionend',
  'transitioncancel' ]
  .forEach(event =>
             gDisplay.addEventListener(event,
                                       event => gEventsReceived.push(event),
                                       false));

function checkEventOrder(...args) {
  // Argument format:
  // Arguments     = ExpectedEvent*, desc
  // ExpectedEvent =
  //       [ target|animationName|transitionProperty, (pseudo,) message ]
  var expectedEvents  = args.slice(0, -1);
  var desc            = args[args.length - 1];
  var isTestingNameOrProperty = expectedEvents.length &&
                                typeof expectedEvents[0][0] == 'string';

  var formatEvent = (target, nameOrProperty, pseudo, message) =>
    isTestingNameOrProperty ?
    `${nameOrProperty}${pseudo}:${message}` :
    `${target.id}${pseudo}:${message}`;

  var actual = gEventsReceived.map(
      event => formatEvent(event.target,
                           event.animationName || event.propertyName,
                           event.pseudoElement, event.type)
    ).join(';');
  var expected = expectedEvents.map(
      event => event.length == 3 ?
               formatEvent(event[0], event[0], event[1], event[2]) :
               formatEvent(event[0], event[0], '', event[1])
    ).join(';');
  is(actual, expected, desc);
  gEventsReceived = [];
}

// 1. TESTS FOR SORTING BY TREE ORDER

// 1a. Test that simultaneous events are sorted by tree order (siblings)

var divs = [ document.createElement('div'),
             document.createElement('div'),
             document.createElement('div') ];
divs.forEach((div, i) => {
  gDisplay.appendChild(div);
  div.setAttribute('style', 'animation: anim 10s');
  div.setAttribute('id', 'div' + i);
  getComputedStyle(div).animationName; // trigger building of animation
});

advance_clock(0);
checkEventOrder([ divs[0], 'animationstart' ],
                [ divs[1], 'animationstart' ],
                [ divs[2], 'animationstart' ],
                'Simultaneous start on siblings');

divs.forEach(div => div.remove());
divs = [];

// 1b. Test that simultaneous events are sorted by tree order (children)

divs = [ document.createElement('div'),
         document.createElement('div'),
         document.createElement('div') ];

// Create the following arrangement:
//
//      display
//       /   \
//    div[0] div[1]
//     /
//   div[2]

gDisplay.appendChild(divs[0]);
gDisplay.appendChild(divs[1]);
divs[0].appendChild(divs[2]);

divs.forEach((div, i) => {
  div.setAttribute('style', 'animation: anim 10s');
  div.setAttribute('id', 'div' + i);
  getComputedStyle(div).animationName; // trigger building of animation
});

advance_clock(0);
checkEventOrder([ divs[0], 'animationstart' ],
                [ divs[2], 'animationstart' ],
                [ divs[1], 'animationstart' ],
                'Simultaneous start on children');

divs.forEach(div => div.remove());
divs = [];

// 1c. Test that simultaneous events are sorted by tree order (pseudos)

divs = [ document.createElement('div'),
         document.createElement('div') ];

// Create the following arrangement:
//
//      display
//         |
//       div[0]
//     ::before,
//     ::after
//        |
//      div[1]

gDisplay.appendChild(divs[0]);
divs[0].appendChild(divs[1]);

divs.forEach((div, i) => {
  div.setAttribute('style', 'animation: anim 10s');
  div.setAttribute('id', 'div' + i);
});

var extraStyle = document.createElement('style');
document.head.appendChild(extraStyle);
var sheet = extraStyle.sheet;
sheet.insertRule('#div0::after { animation: anim 10s }', 0);
sheet.insertRule('#div0::before { animation: anim 10s }', 1);
getComputedStyle(divs[0]).animationName; // build animation
getComputedStyle(divs[1]).animationName; // build animation

advance_clock(0);
checkEventOrder([ divs[0], 'animationstart' ],
                [ divs[0], '::before', 'animationstart' ],
                [ divs[0], '::after', 'animationstart' ],
                [ divs[1], 'animationstart' ],
                'Simultaneous start on pseudo-elements');

divs.forEach(div => div.remove());
divs = [];

sheet = undefined;
extraStyle.remove();
extraStyle = undefined;

// 2. TESTS FOR SORTING BY TIME

// 2a. Test that events are sorted by time

divs = [ document.createElement('div'),
         document.createElement('div') ];
divs.forEach((div, i) => {
  gDisplay.appendChild(div);
  div.setAttribute('style', 'animation: anim 10s');
  div.setAttribute('id', 'div' + i);
});

divs[0].style.animationDelay = '5s';

advance_clock(0);
advance_clock(20000);

checkEventOrder([ divs[1], 'animationstart' ],
                [ divs[0], 'animationstart' ],
                [ divs[1], 'animationend' ],
                [ divs[0], 'animationend' ],
                'Sorting of start and end events by time');

divs.forEach(div => div.remove());
divs = [];

// 2b. Test different events within the one element

var div = document.createElement('div');
gDisplay.appendChild(div);
div.style.animation = 'anim 4s 2, ' +   // Repeat at t=4s
                      'anim 10s 5s, ' + // Start at t=5s
                      'anim 3s';        // End at t=3s
div.setAttribute('id', 'div');
getComputedStyle(div).animationName; // build animation

advance_clock(0);
advance_clock(5000);

checkEventOrder([ div, 'animationstart' ],
                [ div, 'animationstart' ],
                [ div, 'animationend' ],
                [ div, 'animationiteration' ],
                [ div, 'animationstart' ],
                'Sorting of different events by time within an element');

div.remove();
div = undefined;

// 2c. Test negative delay is sorted equal to zero delay but before
//     positive delay

divs = [ document.createElement('div'),
         document.createElement('div'),
         document.createElement('div') ];
divs.forEach((div, i) => {
  gDisplay.appendChild(div);
  div.setAttribute('id', 'div' + i);
});

divs[0].style.animation = 'anim 20s 5s';  // Positive delay, sorts last
divs[1].style.animation = 'anim 20s';     // 0s delay
divs[2].style.animation = 'anim 20s -5s'; // Negative delay, sorts same as
                                          // 0s delay, i.e. use document
                                          // position

advance_clock(0);
advance_clock(5000);
checkEventOrder([ divs[1], 'animationstart' ],
                [ divs[2], 'animationstart' ],
                [ divs[0], 'animationstart' ],
                'Sorting of events including negative delay');

divs.forEach(div => div.remove());
divs = [];

// 3. TESTS FOR SORTING BY animation-name POSITION

// 3a. Test animation-name position

div = document.createElement('div');
gDisplay.appendChild(div);
div.style.animation = 'animA 10s, animB 5s, animC 5s 2';
div.setAttribute('id', 'div');
getComputedStyle(div).animationName; // build animation

advance_clock(0);

checkEventOrder([ 'animA', 'animationstart' ],
                [ 'animB', 'animationstart' ],
                [ 'animC', 'animationstart' ],
                'Sorting of simultaneous animationstart events by ' +
                'animation-name');

advance_clock(5000);

checkEventOrder([ 'animB', 'animationend' ],
                [ 'animC', 'animationiteration' ],
                'Sorting of different types of events by animation-name');

div.remove();
div = undefined;

// 3b. Test time trumps animation-name position

div = document.createElement('div');
gDisplay.appendChild(div);
div.style.animation = 'animA 10s 2s, animB 10s 1s';
div.setAttribute('id', 'div');

advance_clock(0);
advance_clock(3000);

checkEventOrder([ 'animB', 'animationstart' ],
                [ 'animA', 'animationstart' ],
                'Events are sorted by time first, before animation-position');

div.remove();
div = undefined;

// 4. TESTS FOR TRANSITIONS

// 4a. Test sorting transitions by document position

divs = [ document.createElement('div'),
         document.createElement('div') ];
divs.forEach((div, i) => {
  gDisplay.appendChild(div);
  div.style.marginLeft = '0px';
  div.style.transition = 'margin-left 10s';
  div.setAttribute('id', 'div' + i);
});

getComputedStyle(divs[0]).marginLeft;
divs.forEach(div => div.style.marginLeft = '100px');
getComputedStyle(divs[0]).marginLeft;

advance_clock(0);
advance_clock(10000);

checkEventOrder([ divs[0], 'transitionrun' ],
                [ divs[0], 'transitionstart' ],
                [ divs[1], 'transitionrun' ],
                [ divs[1], 'transitionstart' ],
                [ divs[0], 'transitionend' ],
                [ divs[1], 'transitionend' ],
                'Simultaneous transitionrun/start/end on siblings');

divs.forEach(div => div.remove());
divs = [];

// 4b. Test sorting transitions by document position (children)

divs = [ document.createElement('div'),
         document.createElement('div'),
         document.createElement('div') ];

// Create the following arrangement:
//
//      display
//       /   \
//    div[0] div[1]
//     /
//   div[2]

gDisplay.appendChild(divs[0]);
gDisplay.appendChild(divs[1]);
divs[0].appendChild(divs[2]);

divs.forEach((div, i) => {
  div.style.marginLeft = '0px';
  div.style.transition = 'margin-left 10s';
  div.setAttribute('id', 'div' + i);
});

getComputedStyle(divs[0]).marginLeft;
divs.forEach(div => div.style.marginLeft = '100px');
getComputedStyle(divs[0]).marginLeft;

advance_clock(0);
advance_clock(10000);

checkEventOrder([ divs[0], 'transitionrun' ],
                [ divs[0], 'transitionstart' ],
                [ divs[2], 'transitionrun' ],
                [ divs[2], 'transitionstart' ],
                [ divs[1], 'transitionrun' ],
                [ divs[1], 'transitionstart' ],
                [ divs[0], 'transitionend' ],
                [ divs[2], 'transitionend' ],
                [ divs[1], 'transitionend' ],
                'Simultaneous transitionrun/start/end on children');

divs.forEach(div => div.remove());
divs = [];

// 4c. Test sorting transitions by document position (pseudos)

divs = [ document.createElement('div'),
         document.createElement('div') ];

// Create the following arrangement:
//
//      display
//         |
//       div[0]
//     ::before,
//     ::after
//        |
//      div[1]

gDisplay.appendChild(divs[0]);
divs[0].appendChild(divs[1]);

divs.forEach((div, i) => {
  div.setAttribute('id', 'div' + i);
});

extraStyle = document.createElement('style');
document.head.appendChild(extraStyle);
sheet = extraStyle.sheet;
sheet.insertRule('div, #div0::after, #div0::before { ' +
                 ' transition: margin-left 10s; ' +
                 ' margin-left: 0px }', 0);
sheet.insertRule('div.active, #div0.active::after, #div0.active::before { ' +
                 ' margin-left: 100px }', 1);
sheet.insertRule('#div0::after, #div0::before { ' +
                 ' content: " " }', 2);

getComputedStyle(divs[0]).marginLeft;
divs.forEach(div => div.classList.add('active'));
getComputedStyle(divs[0]).marginLeft;

advance_clock(0);
advance_clock(10000);

checkEventOrder([ divs[0], 'transitionrun' ],
                [ divs[0], 'transitionstart' ],
                [ divs[0], '::before', 'transitionrun' ],
                [ divs[0], '::before', 'transitionstart' ],
                [ divs[0], '::after', 'transitionrun' ],
                [ divs[0], '::after', 'transitionstart' ],
                [ divs[1], 'transitionrun' ],
                [ divs[1], 'transitionstart' ],
                [ divs[0], 'transitionend' ],
                [ divs[0], '::before', 'transitionend' ],
                [ divs[0], '::after', 'transitionend' ],
                [ divs[1], 'transitionend' ],
                'Simultaneous transitionrun/start/end on pseudo-elements');

divs.forEach(div => div.remove());
divs = [];

sheet = undefined;
extraStyle.remove();
extraStyle = undefined;

// 4d. Test sorting transitions by time

divs = [ document.createElement('div'),
         document.createElement('div') ];
divs.forEach((div, i) => {
  gDisplay.appendChild(div);
  div.style.marginLeft = '0px';
  div.setAttribute('id', 'div' + i);
});

divs[0].style.transition = 'margin-left 10s';
divs[1].style.transition = 'margin-left 5s';

getComputedStyle(divs[0]).marginLeft;
divs.forEach(div => div.style.marginLeft = '100px');
getComputedStyle(divs[0]).marginLeft;

advance_clock(0);
advance_clock(10000);

checkEventOrder([ divs[0], 'transitionrun' ],
                [ divs[0], 'transitionstart' ],
                [ divs[1], 'transitionrun' ],
                [ divs[1], 'transitionstart' ],
                [ divs[1], 'transitionend' ],
                [ divs[0], 'transitionend' ],
                'Sorting of transitionrun/start/end events by time');

divs.forEach(div => div.remove());
divs = [];

// 4e. Test sorting transitions by time (with delay)

divs = [ document.createElement('div'),
         document.createElement('div') ];
divs.forEach((div, i) => {
  gDisplay.appendChild(div);
  div.style.marginLeft = '0px';
  div.setAttribute('id', 'div' + i);
});

divs[0].style.transition = 'margin-left 5s 5s';
divs[1].style.transition = 'margin-left 5s';

getComputedStyle(divs[0]).marginLeft;
divs.forEach(div => div.style.marginLeft = '100px');
getComputedStyle(divs[0]).marginLeft;

advance_clock(0);
advance_clock(10 * 1000);

checkEventOrder([ divs[0], 'transitionrun' ],
                [ divs[1], 'transitionrun' ],
                [ divs[1], 'transitionstart' ],
                [ divs[0], 'transitionstart' ],
                [ divs[1], 'transitionend' ],
                [ divs[0], 'transitionend' ],
                'Sorting of transitionrun/start/end events by time' +
                '(including delay)');

divs.forEach(div => div.remove());
divs = [];

// 4f. Test sorting transitions by transition-property

div = document.createElement('div');
gDisplay.appendChild(div);
div.style.opacity = '0';
div.style.marginLeft = '0px';
div.style.transition = 'all 5s';

getComputedStyle(div).marginLeft;
div.style.opacity = '1';
div.style.marginLeft = '100px';
getComputedStyle(div).marginLeft;

advance_clock(0);
advance_clock(10000);

checkEventOrder([ 'margin-left', 'transitionrun' ],
                [ 'margin-left', 'transitionstart' ],
                [ 'opacity', 'transitionrun' ],
                [ 'opacity', 'transitionstart' ],
                [ 'margin-left', 'transitionend' ],
                [ 'opacity', 'transitionend' ],
                'Sorting of transitionrun/start/end events by ' +
                'transition-property')

div.remove();
div = undefined;

// 4g. Test document position beats transition-property

divs = [ document.createElement('div'),
         document.createElement('div') ];
divs.forEach((div, i) => {
  gDisplay.appendChild(div);
  div.style.marginLeft = '0px';
  div.style.opacity = '0';
  div.style.transition = 'all 10s';
  div.setAttribute('id', 'div' + i);
});

getComputedStyle(divs[0]).marginLeft;
divs[0].style.opacity = '1';
divs[1].style.marginLeft = '100px';
getComputedStyle(divs[0]).marginLeft;

advance_clock(0);
advance_clock(10000);

checkEventOrder([ divs[0], 'transitionrun' ],
                [ divs[0], 'transitionstart' ],
                [ divs[1], 'transitionrun' ],
                [ divs[1], 'transitionstart' ],
                [ divs[0], 'transitionend' ],
                [ divs[1], 'transitionend' ],
                'Transition events are sorted by document position first, ' +
                'before transition-property');

divs.forEach(div => div.remove());
divs = [];

// 4h. Test time beats transition-property

div = document.createElement('div');
gDisplay.appendChild(div);
div.style.opacity = '0';
div.style.marginLeft = '0px';
div.style.transition = 'margin-left 10s, opacity 5s';

getComputedStyle(div).marginLeft;
div.style.opacity = '1';
div.style.marginLeft = '100px';
getComputedStyle(div).marginLeft;

advance_clock(0);
advance_clock(10000);

checkEventOrder([ 'margin-left', 'transitionrun' ],
                [ 'margin-left', 'transitionstart' ],
                [ 'opacity', 'transitionrun' ],
                [ 'opacity', 'transitionstart' ],
                [ 'opacity', 'transitionend' ],
                [ 'margin-left', 'transitionend' ],
                'Transition events are sorted by time first, before ' +
                'transition-property');

div.remove();
div = undefined;

// 4i. Test sorting transitions by document position (negative delay)

divs = [ document.createElement('div'),
         document.createElement('div') ];
divs.forEach((div, i) => {
  gDisplay.appendChild(div);
  div.style.marginLeft = '0px';
  div.setAttribute('id', 'div' + i);
});

divs[0].style.transition = 'margin-left 10s 5s';
divs[1].style.transition = 'margin-left 10s';

getComputedStyle(divs[0]).marginLeft;
divs.forEach(div => div.style.marginLeft = '100px');
getComputedStyle(divs[0]).marginLeft;

advance_clock(0);
advance_clock(15 * 1000);

checkEventOrder([ divs[0], 'transitionrun' ],
                [ divs[1], 'transitionrun' ],
                [ divs[1], 'transitionstart' ],
                [ divs[0], 'transitionstart' ],
                [ divs[1], 'transitionend' ],
                [ divs[0], 'transitionend' ],
                'Simultaneous transitionrun/start/end on siblings');

divs.forEach(div => div.remove());
divs = [];

// 4j. Test sorting transitions with cancel
// The order of transitioncancel is based on StyleManager.
// So this test looks like wrong result at a glance. However
// the gecko will cancel div1's transition before div2 in this case.

divs = [ document.createElement('div'),
         document.createElement('div') ];
divs.forEach((div, i) => {
  gDisplay.appendChild(div);
  div.style.marginLeft = '0px';
  div.setAttribute('id', 'div' + i);
});

divs[0].style.transition = 'margin-left 10s 5s';
divs[1].style.transition = 'margin-left 10s';

getComputedStyle(divs[0]).marginLeft;
divs.forEach(div => div.style.marginLeft = '100px');
getComputedStyle(divs[0]).marginLeft;

advance_clock(0);
advance_clock(5 * 1000);
divs.forEach(div =>  div.style.display = 'none' );
getComputedStyle(divs[0]).display;
advance_clock(10 * 1000);

checkEventOrder([ divs[0], 'transitionrun' ],
                [ divs[1], 'transitionrun' ],
                [ divs[1], 'transitionstart' ],
                [ divs[0], 'transitionstart' ],
                [ divs[1], 'transitioncancel' ],
                [ divs[0], 'transitioncancel' ],
                'Simultaneous transitionrun/start/cancel on siblings');

divs.forEach(div => div.remove());
divs = [];

SpecialPowers.DOMWindowUtils.restoreNormalRefresh();

</script>
</body>
</html>
